﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using RayDen.Library.Components.HashGrids;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;

namespace RayDen.Library.Components.PhotonMapping {

    

    struct HashGridPhoton
    {
        
    }


    public class IndirectHashGrid : GenericHashGrid<Photon>, IPhotonMap {

        public IndirectHashGrid(int max) : base(max)
        {
            NormalThreshold = 0.001f;
            Radius = 0.05f;
            CellSizeScale = 2.21f;
            MaxPhotonsPerCell = 16;
        }


        public void QueryMap(ref Point pos, float maxDist, int nphotons, out Photon[] result) {

            var ix = this.hashGrid[(int)this.Hash(ref pos)];
            if (ix == null)
            {
                result = new Photon[0];
                return;
            }

            result = new Photon[Math.Min(ix.Length, nphotons)];


            for (int i = 0; i < Math.Min(ix.Length, nphotons); i++)
            {
                result[i] = this.hitPoints[ix[i]].Data;
            }
        }

        public void Store(ref RgbSpectrum power, ref Point position, ref Vector dir) {
            if (this.photonsCount > (this.maxPhotons - 2))
                return;



            if (power.Length <= 0f)
                return;
            var dirx = MathLab.Clamp(dir.z, -1f, 1f);
            int theta = Convert.ToInt32(Math.Acos(dirx) * (256.0d / Math.PI));
            if (theta > 255)
                theta = 255;
            var thetaB = (byte)theta;
            int phi = Convert.ToInt32(Math.Atan2(dir.y, dir.x) * (256.0d / 2.0 * Math.PI));
            if (phi > 255)
                phi = 255;
            else
                if (phi < 0)
                    phi = phi + 256;
            var phiB = (byte)phi;
            base.Store(ref position,
                       new Photon()
                       {
                           Position = position,
                           Power = power,
                           Theta = thetaB,
                           Phi = phiB
                       });
            this.photonsCount++;
        }
        public void Store(RgbSpectrum power, Point position, Vector dir, Normal shadeN)
        {
            if (this.photonsCount > (this.maxPhotons - 2))
                return;



            if (power.Length <= 0f)
                return;
            var dirx = MathLab.Clamp(dir.z, -1f, 1f);
            int theta = Convert.ToInt32(Math.Acos(dirx) * (256.0d / Math.PI));
            if (theta > 255)
                theta = 255;
            var thetaB = (byte)theta;
            int phi = Convert.ToInt32(Math.Atan2(dir.y, dir.x) * (256.0d / 2.0 * Math.PI));
            if (phi > 255)
                phi = 255;
            else
                if (phi < 0)
                    phi = phi + 256;
            var phiB = (byte)phi;
            base.Store(ref position,
                                   new Photon()
                                   {
                                       Position = position,
                                       Power = power,
                                       Theta = thetaB,
                                       Phi = phiB
                                   });
            this.photonsCount++;
        }


        public new void ReHash() {
            if (this.hitPoints.Count == 0)
                return;
            Tracer.TraceLine("Rehashing...");
            var hitPointsCount = this.hitPoints.Count;
            this.hpbbox = new AABB();
            this.hpbbox.Reset();
            for (int n = 0; n < this.hitPoints.Count; n++) {
                this.hpbbox.fit(this.hitPoints[n].Position);

            }
            Vector ssize = this.hpbbox.Size;
            float photonRadius1 = Radius;
            //((ssize.x + ssize.y + ssize.z) / 3.0f) / (ssize.Length2());

            // Expand the bounding box by used radius
            this.hpbbox.Expand(photonRadius1);

            // Initialize hit points field
            float photonRadius2 = photonRadius1 * photonRadius1;

            // Calculate the size of the grid cell
            float maxPhotonRadius2 = 0f;
            for (int i = 0; i < hitPointsCount; ++i) {
                var hp = this.hitPoints[i];

                //if (hp.type == HitPointType.SURFACE)
                maxPhotonRadius2 = Math.Max(maxPhotonRadius2, photonRadius2);
            }

            float cellSize = MathLab.Sqrt(maxPhotonRadius2) * CellSizeScale;
            //std::cerr << "Hash grid cell size: " << cellSize <<std::endl;
            this.invCellSize = 1f / cellSize;
            if (float.IsInfinity(this.invCellSize))
                throw new ArgumentException("Cell size is invalid");

            // TODO: add a tunable parameter for hashgrid size
            this.hashGridSize = (uint)hitPointsCount;


            var hg = Enumerable.Repeat<List<int>>(null, 10000).ToList();

            //for (var i = 0; i < this.hashGridSize; ++i)this.hashGrid.Add(null);

            //std::cerr << "Building hit points hash grid:" << std::endl;
            //std::cerr << "  0k/" << hitPointsCount / 1000 << "k" <<std::endl;
            //unsigned int maxPathCount = 0;
            //double lastPrintTime = WallClockTime();
            ulong entryCount = 0;

            Console.Write("Building grid [");
            var hppMin = (Vector)this.hpbbox.Min;

            for (var i = 0; i < hitPointsCount; ++i) {
                //if (WallClockTime() - lastPrintTime > 2.0) {
                //    std::cerr << "  " << i / 1000 << "k/" << hitPointsCount / 1000 << "k" <<std::endl;
                //    lastPrintTime = WallClockTime();
                //}

                var hp = this.hitPoints[i];
                this.hpbbox.fit(this.hitPoints[i].Position - photonRadius1);
                this.hpbbox.fit(this.hitPoints[i].Position + photonRadius1);
                {
                    float photonRadius = MathLab.Sqrt(maxPhotonRadius2);
                    Vector rad = new Vector(photonRadius, photonRadius, photonRadius);
                    Vector bMin = ((hp.Position - rad) - hppMin) * this.invCellSize;
                    Vector bMax = ((hp.Position + rad) - hppMin) * this.invCellSize;


                    var hgl = new List<int>(MaxPhotonsPerCell);
                    hgl.Add(i);
                    for (int iz = Math.Abs((int)bMin.z); iz <= Math.Abs((int)bMax.z); iz++) {
                        for (int iy = Math.Abs((int)bMin.y); iy <= Math.Abs((int)bMax.y); iy++) {
                            for (int ix = Math.Abs((int)bMin.x); ix <= Math.Abs((int)bMax.x); ix++) {
                                int hv = (int)this.Hash(ix, iy, iz);

                                if (hg.Count < hv) {
                                    hg.AddRange(Enumerable.Repeat<List<int>>(null, hv + 1));
                                }

                                if (hg[hv] == null) {
                                    hg[hv] = hgl;
                                }
                                else {
                                    hg[hv].AddRange(hgl);
                                }

                                //new List<int>(MaxPhotonsPerCell);

                                if (hg[hv].Count > MaxPhotonsPerCell) {
                                    MaxPhotonsPerCell = hg[hv].Count;
                                }

                                //hg[hv].Add(i);

                                ++entryCount;



                                /*// hashGrid[hv].size() is very slow to execute
                                if (hashGrid[hv].size() > maxPathCount)
                                    maxPathCount = hashGrid[hv].size();*/
                            }
                        }
                    }

                    if ((i % 100) == 0) {
                        Console.Write("*");
                    }
                }
            }
            Console.Write("]. Complete");
            Tracer.TraceLine();
            //std::cerr << "Max. hit points in a single hash grid entry: " << maxPathCount << std::endl;
            //std::cerr << "Total hash grid entry: " << entryCount << std::endl;
            //std::cerr << "Avg. hit points in a single hash grid entry: " << entryCount / hashGridSize << std::endl;

            // HashGrid debug code
            /*for (unsigned int i = 0; i < hashGridSize; ++i) {
                if (hashGrid[i]) {
                    if (hashGrid[i].size() > 10) {
                        std::cerr << "HashGrid[" << i << "].size() = " <<hashGrid[i].size() << std::endl;
                    }
                }
            }*/
            //var hgc = hg.Where(item=>item != null).Sum(item => item.Count);
            var hgc = this.PhotonsCount;
            Tracer.TraceLine("Total Photons {0} Item Size {1} Total Size {2:F1} KBytes ", this.hitPoints.Count, Marshal.SizeOf(typeof(Photon)), this.hitPoints.Count * Marshal.SizeOf(typeof(Photon)) / 1000f);
            Tracer.TraceLine("Total Items in Grid {0} Item Size {1} Total Size {2:F1} KBytes ", hgc, 4, (hgc * 4f) / 1000f);
            this.hashGrid = hg.Select(item => item != null ? item.ToArray() : null).ToArray();
            GC.Collect();
            Tracer.TraceLine("Complete...");
        }


        private uint Hash(ref Vector i) {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }
        private uint Hash(Vector i) {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }
        public static Vector PhotonDirection(ref Photon p) {
            return new Vector(
                DirectionDiscretizationHelper.SinTheta(p.Theta) * DirectionDiscretizationHelper.CosPhi(p.Phi),
                DirectionDiscretizationHelper.SinTheta(p.Theta) * DirectionDiscretizationHelper.SinPhi(p.Phi),
                DirectionDiscretizationHelper.CosTheta(p.Theta)
                );
        }

        public Vector IrradianceEstimate(ref Point pos, ref Normal n, float max_dist, int nphotons)
        {
            try {
                var pCell = this.hashGrid[(int)this.Hash(ref pos)];
                var photons = new List<int>();

                var irrad = new Vector();
                if (pCell == null || pCell.Count() == 0) {
                    return irrad;
                }

                photons.AddRange(pCell);
                /*
        if (photons.Count < nphotons)
        {
            var minp = this.hashGrid[(int)this.Hash(pos - max_dist)];
            var maxp = this.hashGrid[(int)this.Hash(pos + max_dist)];
            if (minp != null) photons.AddRange(minp);
            if (maxp != null) photons.AddRange(maxp);
        }
              */
                int nc = 0;
                for (int index = 0; index < Math.Min(photons.Count(), nphotons); index++) {
                    var photon = hitPoints[photons[index]];
                    Vector pdir =
                        //photon.Direction;
                        PhotonDirection(ref photon.Data);
                    var dot = Vector.Dot(ref pdir, ref n);
                    var dist = (photon.Position - (Vector)pos).Length2();
                    if (dot > NormalThreshold && dist < max_dist * max_dist) {
                        irrad += (Vector)photon.Data.Power; //*BRDF
                        nc++;
                    }
                }
                var tmp = nc * 1.0f / (MathLab.M_PI);
                irrad = irrad * tmp;
                return irrad;
            }
            catch (Exception ex) {
                Tracer.TraceLine(ex.Message);
                return new Vector();
            }
        }


        #region IPhotonMap Members

        public new Photon[] GetPlainData() {
            return this.hitPoints.Select(item=>item.Data).ToArray();
        }

        public override void ScalePhotonMap(float scale) {
            foreach (var hitPoint in hitPoints)
            {
                hitPoint.Data.ScalePower(scale);
            }
        }

    
      

        #endregion
    }
}